<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body>
  <div id="root">
    <div class="c1">
      <div title="tt1" id="id">{{ name }}</div>
      <div title="tt2">{{ age }}</div>
      <div title="tt3">{{ gender }}</div>
      <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
      </ul>
    </div>
  </div>
  <script>
    /** 虚拟 DOM 构造函数 */
    class VNode {
      constructor(tag, data, value, type) {
        this.tag = tag && tag.toLowerCase();
        this.data = data;
        this.value = value;
        this.type = type;
        this.children = [];
      }

      appendChild(vnode) {
        this.children.push(vnode);
      }
    }
    /** 由 HTML DOM -> VNode: 将这个函数当做 compiler 函数 */
    function getVNode(node) {
      let nodeType = node.nodeType;
      let _vnode = null;
      if (nodeType === 1) {
        // 元素
        let nodeName = node.nodeName;
        let attrs = node.attributes;
        let _attrObj = {};
        for (let i = 0; i < attrs.length; i++) { // attrs[ i ] 属性节点 ( nodeType == 2 )
          _attrObj[attrs[i].nodeName] = attrs[i].nodeValue;
        }
        _vnode = new VNode(nodeName, _attrObj, undefined, nodeType);

        // 考虑 node 的子元素
        let childNodes = node.childNodes;
        for (let i = 0; i < childNodes.length; i++) {
          _vnode.appendChild(getVNode(childNodes[i])); // 递归
        }

      } else if (nodeType === 3) {

        _vnode = new VNode(undefined, undefined, node.nodeValue, nodeType);
      }

      return _vnode;
    }

    /** 将虚拟 DOM 转换成真正的 DOM */
    function parseVNode(vnode) {
      // 创建 真实的 DOM
      let type = vnode.type;
      let _node = null;
      if (type === 3) {
        return document.createTextNode(vnode.value); // 创建文本节点
      } else if (type === 1) {

        _node = document.createElement(vnode.tag);

        // 属性
        let data = vnode.data; // 现在这个 data 是键值对
        Object.keys(data).forEach((key) => {
          let attrName = key;
          let attrValue = data[key];
          _node.setAttribute(attrName, attrValue);
        });

        // 子元素
        let children = vnode.children;
        children.forEach(subvnode => {
          _node.appendChild(parseVNode(subvnode)); // 递归转换子元素 ( 虚拟 DOM )
        });

        return _node;
      }

    }

    let rkuohao = /\{\{(.+?)\}\}/g;
    /** 根据路径 访问对象成员 */
    function getValueByPath(obj, path) {
      let paths = path.split('.'); // [ xxx, yyy, zzz ]
      let res = obj;
      let prop;
      while (prop = paths.shift()) {
        res = res[prop];
      }
      return res;
    }
    /** 将 带有 坑的 Vnode 与数据 data 结合, 得到 填充数据的 VNode: 模拟 AST -> VNode */
    function combine(vnode, data) {
      let _type = vnode.type;
      let _data = vnode.data;
      let _value = vnode.value;
      let _tag = vnode.tag;
      let _children = vnode.children;


      let _vnode = null;

      if (_type === 3) { // 文本节点 

        // 对文本处理
        _value = _value.replace(rkuohao, function (_, g) {
          return getValueByPath(data, g.trim()); // 除了 get 读取器
        });

        _vnode = new VNode(_tag, _data, _value, _type)

      } else if (_type === 1) { // 元素节点
        _vnode = new VNode(_tag, _data, _value, _type);
        _children.forEach(_subvnode => _vnode.appendChild(combine(_subvnode, data)));
      }

      return _vnode;
    }


    function JGVue(options) {
      this._data = options.data;
      let elm = document.querySelector(options.el); // vue 是字符串, 这里是 DOM 
      this._template = elm;
      this._parent = elm.parentNode;

      this.initData(); // 将 data 进行响应式转换, 进行代理

      this.mount(); // 挂载
    }

    JGVue.prototype.mount = function () {
      // 需要提供一个 render 方法: 生成 虚拟 DOM
      this.render = this.createRenderFn() // 带有缓存 ( Vue 本身是可以带有 render 成员 )

      this.mountComponent();
    }
    JGVue.prototype.mountComponent = function () {
      // 执行 mountComponent() 函数 
      let mount = () => { // 这里是一个函数, 函数的 this 默认是全局对象 "函数调用模式"
        this.update(this.render())
      }
      mount.call(this); // 本质应该交给 watcher 来调用, 但是还没有讲到这里
    }

    // 这里是生成 render 函数, 目的是缓存 抽象语法树 ( 我们使用 虚拟 DOM 来模拟 )
    JGVue.prototype.createRenderFn = function () {
      let ast = getVNode(this._template);
      // Vue: 将 AST + data => VNode
      // 我们: 带有坑的 VNode + data => 含有数据的 VNode
      return function render() {
        // 将 带有 坑的 VNode 转换为 待数据的 VNode
        let _tmp = combine(ast, this._data);
        return _tmp;
      }
    }

    // 将虚拟 DOM 渲染到页面中: diff 算法就在里
    JGVue.prototype.update = function (vnode) {
      // 简化, 直接生成 HTML DOM replaceChild 到页面中
      // 父元素.replaceChild( 新元素, 旧元素 )
      let realDOM = parseVNode(vnode);

      // debugger;
      // let _ = 0;

      this._parent.replaceChild(realDOM, document.querySelector('#root'));
      // 这个算法是不负责任的: 
      // 每次会将页面中的 DOM 全部替换
    }

    // 响应式化的部分
    let ARRAY_METHOD = [
      'push',
      'pop',
      'shift',
      'unshift',
      'reverse',
      'sort',
      'splice',
    ];
    let array_methods = Object.create(Array.prototype);
    ARRAY_METHOD.forEach(method => {
      array_methods[method] = function () {
        // 调用原来的方法
        console.log('调用的是拦截的 ' + method + ' 方法');

        // 将数据进行响应式化
        for (let i = 0; i < arguments.length; i++) {
          reactify(arguments[i]);
        }

        let res = Array.prototype[method].apply(this, arguments);
        // Array.prototype[ method ].call( this, ...arguments ); // 类比
        return res;
      }
    });



    // 简化后的版本 
    function defineReactive(target, key, value, enumerable) {
      // 折中处理后, this 就是 Vue 实例
      let that = this;

      // 函数内部就是一个局部作用域, 这个 value 就只在函数内使用的变量 ( 闭包 )
      if (typeof value === 'object' && value != null && !Array.isArray(value)) {
        // 是非数组的引用类型
        reactify(value); // 递归
      }

      Object.defineProperty(target, key, {
        configurable: true,
        enumerable: !!enumerable,

        get() {
          console.log(`读取 ${key} 属性`); // 额外
          return value;
        },
        set(newVal) {
          console.log(`设置 ${key} 属性为: ${newVal}`); // 额外

          // 临时的处理办法
          if (typeof newVal === 'object' && newVal != null) {
            value = reactify(newVal); // 由于这个方法现在暂时只是过渡( 不安全 )
          } else {
            value = newVal;
          }


          // 模板刷新 ( 这现在是假的, 只是演示 )
          // vue 实例??? watcher 就不会有这个问题
          that.mountComponent();

        }
      });
    }


    // 将对象 o 响应式化
    function reactify(o, vm) {
      let keys = Object.keys(o);

      for (let i = 0; i < keys.length; i++) {
        let key = keys[i]; // 属性名
        let value = o[key];
        if (Array.isArray(value)) {
          // 数组
          value.__proto__ = array_methods; // 数组就响应式了
          for (let j = 0; j < value.length; j++) {
            reactify(value[j], vm); // 递归
          }
        } else {
          // 对象或值类型
          defineReactive.call(vm, o, key, value, true);
        }

        // 只需要在这里添加代理即可 ( 问题: 在这里写的代码是会递归 )
        // 如果在这里将 属性映射到 Vue 实例上, 那么就表示 Vue 实例可以使用属性 key
        // { 
        //   data:  { name: 'jack', child: { name: 'jim' } }
        // }
      }
    }


    JGVue.prototype.initData = function () {
      // 遍历 this._data 的成员, 将 属性转换为响应式 ( 上 ), 将 直接属性, 代理到 实例上
      let keys = Object.keys(this._data);

      // 响应式化
      for (let i = 0; i < keys.length; i++) {
        // 这里将 对象 this._data[ keys[ i ] ] 变成响应式的
        reactify(this._data, this);
      }

      // 代理
      for (let i = 0; i < keys.length; i++) {
        // 将 this._data[ keys[ i ] ] 映射到 this[ keys[ i ] ] 上
        // 就是要 让 this 提供 keys[ i ] 这个属性
        // 在访问这个属性的时候 相当于在 访文this._data 的这个属性

        proxy(this, '_data', keys[i]);
      }
    };

    /** 将 某一个对象的属性 访问 映射到 对象的某一个属性成员上 */
    // target.key   实际上读取的就是 target.prop.key(我们获取data里的数据也是直接 this.name 而不是 this.data.name)
    function proxy(target, prop, key) {
      Object.defineProperty(target, key, {
        enumerable: true,
        configurable: true,
        get() {
          return target[prop][key];
        },
        set(newVal) {
          target[prop][key] = newVal;
        }
      });
    }




    let app = new JGVue({
      el: '#root',
      data: {
        name: '张三'
        , age: 19
        , gender: '难'
        , datas: [
          { info: '好难' },
          { info: '太难' },
          { info: '真的难么?' },
          { info: '练习一下' },
        ]
      },

    });


  </script>
</body>

</html>